#!/bin/sh

#set -x

cd $(dirname "$0")

default_path=".."
default_cmd="sparse \$file"
default_args="$SPARSE_TEST_ARGS"
tests_list=""
prog_name=`basename $0`

if [ ! -x "$default_path/sparse-llvm" ]; then
	disabled_cmds="sparsec sparsei sparse-llvm sparse-llvm-dis"
fi
if [ ! -x "$default_path/scheck" ]; then
	disabled_cmds="$disabled_cmds scheck"
fi

# flags:
#	- some tests gave an unexpected result
failed=0

# counts:
#	- tests that have not been converted to test-suite format
#	- tests that are disabled
#	- tests that passed
#	- tests that failed
#	- tests that failed but are known to fail
unhandled_tests=0
disabled_tests=0
ok_tests=0
ko_tests=0
known_ko_tests=0

# defaults to not verbose
[ -z "$V" ] && V=0
vquiet=""
quiet=0
abort=0


##
# verbose(string) - prints string if we are in verbose mode
verbose()
{
	[ "$V" -eq "1" ] && echo "        $1"
	return 0
}

##
# warning(string) - prints a warning
warning()
{
	[ "$quiet" -ne 1 ] && echo "warning: $1"
	return 0
}

##
# error(string[, die]) - prints an error and exits with value die if given
error()
{
	[ "$quiet" -ne 1 ] && echo "error: $1"
	[ -n "$2" ] && exit $2
	return 0
}


##
# get_tag_value(file) - get the 'check-<...>' tags & values
get_tag_value()
{
	check_name=""
	check_command="$default_cmd"
	check_exit_value=0
	check_timeout=0
	check_known_to_fail=0
	check_error_ignore=0
	check_output_ignore=0
	check_output_contains=0
	check_output_excludes=0
	check_output_pattern=0
	check_output_match=0
	check_output_returns=0
	check_arch_ignore=""
	check_arch_only=""
	check_assert=""
	check_cpp_if=""

	lines=$(grep '^ \* check-[a-z-]*' $1 | \
		sed -e 's/^ \* \(check-[a-z-]*:*\) *\(.*\)$/\1 \2/')

	while read tag val; do
		#echo "-> tag: '$tag'"
		#echo "-> val: '$val'"
		case $tag in
		check-name:)		check_name="$val" ;;
		check-command:)		check_command="$val" ;;
		check-exit-value:)	check_exit_value="$val" ;;
		check-timeout:)		[ -z "$val" ] && val=1
					check_timeout="$val" ;;
		check-known-to-fail)	check_known_to_fail=1 ;;
		check-error-ignore)	check_error_ignore=1 ;;
		check-output-ignore)	check_output_ignore=1 ;;
		check-output-contains:)	check_output_contains=1 ;;
		check-output-excludes:)	check_output_excludes=1 ;;
		check-output-pattern)	check_output_pattern=1 ;;
		check-output-match)	check_output_match=1 ;;
		check-output-returns:)	check_output_returns=1 ;;
		check-arch-ignore:)	arch=$(uname -m)
					check_arch_ignore="$val" ;;
		check-arch-only:)	arch=$(uname -m)
					check_arch_only="$val" ;;
		check-assert:)		check_assert="$val" ;;
		check-cpp-if:)		check_cpp_if="$val" ;;

		check-description:)	;;	# ignore
		check-note:)		;;	# ignore
		check-warning:)		;;	# ignore
		check-error-start)	;;	# ignore
		check-error-end)	;;	# ignore
		check-output-start)	;;	# ignore
		check-output-end)	;;	# ignore
		check-should-pass)	;;	# ignore, unused annotation
		check-should-fail)	;;	# ignore, unused annotation
		check-should-warn)	;;	# ignore, unused annotation
		check-*)		error "$1: unknown tag '$tag'" 1 ;;
		esac
	done << EOT
	$lines
EOT
}

##
# helper for has_(each|none)_patterns()
has_patterns()
{
	ifile="$1"
	patt="$2"
	ofile="$3"
	cmp="$4"
	msg="$5"
	grep "$patt:" "$ifile" | \
	sed -e "s/^.*$patt: *\(.*\)$/\1/" | \
	while read val; do
		grep -s -q "$val" "$ofile"
		if [ "$?" $cmp 0 ]; then
			error "	Pattern '$val' unexpectedly $msg"
			return 1
		fi
	done

	return $?
}

##
# has_each_patterns(ifile tag ofile) - does ofile contains some
#                        of the patterns given by ifile's tags?
#
# returns 0 if all present, 1 otherwise
has_each_patterns()
{
	has_patterns "$1" "$2" "$4" -ne "$3"
}

##
# has_none_patterns(ifile tag ofile) - does ofile contains some
#                        of the patterns given by ifile's tags?
#
# returns 1 if any present, 0 otherwise
has_none_patterns()
{
	has_patterns "$1" "$2" "$4" -eq "$3"
}

##
# minmax_patterns(ifile tag ofile) - does ofile contains the
#                        the patterns given by ifile's tags
#                        the right number of time?
minmax_patterns()
{
	ifile="$1"
	patt="$2"
	ofile="$3"
	grep "$patt([0-9-]*\(, *\)*[0-9-]*):" "$ifile" | \
	sed -e "s/^.*$patt(\([0-9]*\)): *\(.*\)/\1 eq \2/" \
	    -e "s/^.*$patt(\([0-9-]*\), *\([0-9-]*\)): *\(.*\)/\1 \2 \3/" | \
	while read min max pat; do
		n=$(grep -s "$pat" "$ofile" | wc -l)
		if [ "$max" = "eq" ]; then
		    if [ "$n" -ne "$min" ]; then
			error "	Pattern '$pat' expected $min times but got $n times"
			return 1
		    fi
		    continue
		fi
		if [ "$min" != '-' ]; then
		    if [ "$n" -lt "$min" ]; then
			error "	Pattern '$pat' expected min $min times but got $n times"
			return 1
		    fi
		fi
		if [ "$max" != '-' ]; then
		    if [ "$n" -gt "$max" ]; then
			error "	Pattern '$pat' expected max $max times but got $n times"
			return 1
		    fi
		fi
	done

	return $?
}

##
match_patterns()
{
	ifile="$1"
	patt="$2"
	ofile="$3"
	grep "$patt" "$ifile" | sed -e "s/^.*$patt(\(.*\)): *\(.*\)$/\1 \2/" | \
	while read ins pat; do
		grep -s "^	$ins\\.*[0-9]* " "$ofile" | grep -v -s -q "$pat"
		if [ "$?" -ne 1 ]; then
			error "	IR doesn't match '$pat'"
			return 1
		fi
	done

	return $?
}

##
return_patterns()
{
	ifile="$1"
	patt="$2"
	ofile="$3"
	grep "$patt:" "$ifile" | sed -e "s/^.*$patt: *\(.*\)$/\1/" | \
	while read ret; do
		grep -s "^	ret\\.[0-9]" "$ofile" | grep -v -s -q "[ \$]${ret}\$"
		if [ "$?" -ne 1 ]; then
			error "	Return doesn't match '$ret'"
			return 1
		fi
	done

	return $?
}

##
# arg_file(filename) - checks if filename exists
arg_file()
{
	[ -z "$1" ] && {
		do_usage
		exit 1
	}
	[ -e "$1" ] || {
		error "Can't open file $1"
		exit 1
	}
	return 0
}


##
do_usage()
{
echo "$prog_name - a tiny automatic testing script"
echo "Usage: $prog_name [option(s)] [command] [arguments]"
echo
echo "options:"
echo "    -a|--abort                 Abort the tests as soon as one fails."
echo "    -q|--quiet                 Be extra quiet while running the tests."
echo "    --args='...'               Add these options to the test command."
echo
echo "commands:"
echo "    [file ...]                 Runs the test suite on the given file(s)."
echo "                               If a directory is given, run only those files."
echo "                               If no file is given, run the whole testsuite."
echo "    single file                Run the test in 'file'."
echo "    format file [name [cmd]]   Help writing a new test case using cmd."
echo
echo "    [command] help             Print usage."
}

disable()
{
	disabled_tests=$(($disabled_tests + 1))
	if [ -z "$vquiet" ]; then
		echo "  SKIP    $1 ($2)"
	fi
}

##
# do_test(file) - tries to validate a test case
#
# it "parses" file, looking for check-* tags and tries to validate
# the test against an expected result
# returns:
#	- 0 if the test passed,
#	- 1 if it failed,
#	- 2 if it is not a "test-suite" test.
#	- 3 if the test is disabled.
do_test()
{
	test_failed=0
	file="$1"
	quiet=0

	get_tag_value $file

	# can this test be handled by test-suite ?
	# (it has to have a check-name key in it)
	if [ "$check_name" = "" ]; then
		warning "$file: test unhandled"
		unhandled_tests=$(($unhandled_tests + 1))
		return 2
	fi
	test_name="$check_name"

	# does the test provide a specific command ?
	if [ "$check_command" = "" ]; then
		check_command="$defaut_command"
	fi

	# check for disabled commands
	set -- $check_command
	base_cmd=$1
	for i in $disabled_cmds; do
		if [ "$i" = "$base_cmd" ] ; then
			disable "$test_name" "$file"
			return 3
		fi
	done
	if [ "$check_arch_ignore" != "" ]; then
		if echo $arch | egrep -q -w "$check_arch_ignore"; then
			disable "$test_name" "$file"
			return 3
		fi
	fi
	if [ "$check_arch_only" != "" ]; then
		if ! (echo $arch | egrep -q -w "$check_arch_only"); then
			disable "$test_name" "$file"
			return 3
		fi
	fi
	if [ "$check_assert" != "" ]; then
		res=$(../sparse - 2>&1 >/dev/null <<- EOF
			_Static_assert($check_assert, "$check_assert");
			EOF
		)
		if [ "$res" != "" ]; then
			disable "$test_name" "$file"
			return 3
		fi
	fi
	if [ "$check_cpp_if" != "" ]; then
		res=$(../sparse -E - 2>/dev/null <<- EOF
			#if !($check_cpp_if)
			fail
			#endif
			EOF
		)
		if [ "$res" != "" ]; then
			disable "$test_name" "$file"
			return 3
		fi
	fi

	if [ -z "$vquiet" ]; then
		echo "  TEST    $test_name ($file)"
	fi

	verbose "Using command       : $(echo "$@")"

	# grab the expected exit value
	expected_exit_value=$check_exit_value
	verbose "Expecting exit value: $expected_exit_value"

	# do we want a timeout?
	pre_cmd=""
	if [ $check_timeout -ne 0 ]; then
		pre_cmd="timeout $check_timeout"
	fi

	shift
	# launch the test command and
	# grab the actual output & exit value
	eval $pre_cmd $default_path/$base_cmd $default_args "$@" \
		1> $file.output.got 2> $file.error.got
	actual_exit_value=$?

	must_fail=$check_known_to_fail
	[ $must_fail -eq 1 ] && [ $V -eq 0 ] && quiet=1
	known_ko_tests=$(($known_ko_tests + $must_fail))

	for stream in error output; do
		eval ignore=\$check_${stream}_ignore
		[ $ignore -eq 1 ] && continue

		# grab the expected output
		sed -n "/check-$stream-start/,/check-$stream-end/p" $file \
		| grep -v check-$stream > "$file".$stream.expected

		diff -u "$file".$stream.expected "$file".$stream.got > "$file".$stream.diff
		if [ "$?" -ne "0" ]; then
			error "actual $stream text does not match expected $stream text."
			error  "see $file.$stream.* for further investigation."
			[ $quiet -ne 1 ] && cat "$file".$stream.diff
			test_failed=1
		fi
	done

	if [ "$actual_exit_value" -ne "$expected_exit_value" ]; then
		error "Actual exit value does not match the expected one."
		error "expected $expected_exit_value, got $actual_exit_value."
		test_failed=1
	fi

	# verify the 'check-output-contains/excludes' tags
	if [ $check_output_contains -eq 1 ]; then
		has_each_patterns "$file" 'check-output-contains' absent $file.output.got
		if [ "$?" -ne "0" ]; then
			test_failed=1
		fi
	fi
	if [ $check_output_excludes -eq 1 ]; then
		has_none_patterns "$file" 'check-output-excludes' present $file.output.got
		if [ "$?" -ne "0" ]; then
			test_failed=1
		fi
	fi
	if [ $check_output_pattern -eq 1 ]; then
		# verify the 'check-output-pattern(...)' tags
		minmax_patterns "$file" 'check-output-pattern' $file.output.got
		if [ "$?" -ne "0" ]; then
			test_failed=1
		fi
	fi
	if [ $check_output_match -eq 1 ]; then
		# verify the 'check-output-match($insn): $patt' tags
		match_patterns "$file" 'check-output-match' $file.output.got
		if [ "$?" -ne "0" ]; then
			test_failed=1
		fi
	fi
	if [ $check_output_returns -eq 1 ]; then
		# verify the 'check-output-return: $value' tags
		return_patterns "$file" 'check-output-returns' $file.output.got
		if [ "$?" -ne "0" ]; then
			test_failed=1
		fi
	fi

	if [ "$must_fail" -eq "1" ]; then
		if [ "$test_failed" -eq "1" ]; then
			[ -z "$vquiet" ] && \
			echo "info: XFAIL: test '$file' is known to fail"
		else
			echo "error: XPASS: test '$file' is known to fail but succeed!"
		fi
	else
		if [ "$test_failed" -eq "1" ]; then
			echo "error: FAIL: test '$file' failed"
		else
			[ "$V" -ne "0" ] && \
			echo "info: PASS: test '$file' passed"
		fi
	fi

	if [ "$test_failed" -ne "$must_fail" ]; then
		[ $abort -eq 1 ] && exit 1
		test_failed=1
		failed=1
	fi

	if [ "$test_failed" -eq "1" ]; then
		ko_tests=$(($ko_tests + 1))
	else
		ok_tests=$(($ok_tests + 1))
		rm -f $file.{error,output}.{expected,got,diff}
	fi
	return $test_failed
}

do_test_suite()
{
	for i in $tests_list; do
		do_test "$i"
	done

	OK=OK
	[ $failed -eq 0 ] || OK=KO

	# prints some numbers
	tests_nr=$(($ok_tests + $ko_tests))
	echo "$OK: out of $tests_nr tests, $ok_tests passed, $ko_tests failed"
	if [ "$known_ko_tests" -ne 0 ]; then
		echo "	$known_ko_tests of them are known to fail"
	fi
	if [ "$unhandled_tests" -ne "0" ]; then
		echo "	$unhandled_tests tests could not be handled by $prog_name"
	fi
	if [ "$disabled_tests" -ne "0" ]; then
		echo "	$disabled_tests tests were disabled"
	fi
}

##
do_format_help() {
echo "Usage: $prog_name [option(s)] [--]format file [name [cmd]]"
echo
echo "options:"
echo "    -a                         append the created test to the input file"
echo "    -f                         write a test known to fail"
echo "    -l                         write a test for linearized code"
echo "    -r                         write a test for linearized code returning 1"
echo "    -p                         write a test for pre-processing"
echo "    -s                         write a test for symbolic checking"
echo
echo "argument(s):"
echo "    file                       file containing the test case(s)"
echo "    name                       name for the test case (defaults to file)"
echo "    cmd                        command to be used (defaults to 'sparse \$file')"
}

##
# do_format([options,] file[, name[, cmd]]) - helps a test writer to format test-suite tags
do_format()
{
	def_cmd="$default_cmd"
	append=0
	linear=0
	fail=0
	ret=''

	while [ $# -gt 0 ] ; do
		case "$1" in
		-a)
			append=1 ;;
		-f)
			fail=1 ;;
		-l)
			def_cmd='test-linearize -Wno-decl $file'
			linear=1 ;;
		-r)
			def_cmd='test-linearize -Wno-decl $file'
			ret=1 ;;
		-p)
			def_cmd='sparse -E $file' ;;
		-s)
			def_cmd='scheck $file' ;;

		help|-*)
			do_format_help
			return 0
			;;
		*)	break ;;
		esac
		shift
		continue
	done

	if [ $# -lt 1 -o $# -gt 3 ]; then
		do_format_help
		return 0
	fi

	arg_file "$1" || return 1

	file="$1"
	fname="$2"
	[ -z "$fname" ] && fname="$(basename "$1" .c)"
	fcmd="$3"
	[ -z "$fcmd" ] && fcmd="$def_cmd"

	cmd=`eval echo $default_path/$fcmd`
	$cmd 1> $file.output.got 2> $file.error.got
	fexit_value=$?
	[ $append != 0 ] && exec >> $file
	cat <<_EOF

/*
 * check-name: $fname
_EOF
	if [ "$fcmd" != "$default_cmd" ]; then
		echo " * check-command: $fcmd"
	fi
	if [ "$fexit_value" -ne "0" ]; then
		echo " * check-exit-value: $fexit_value"
	fi
	if [ $fail != 0 ]; then
		echo " * check-known-to-fail"
	fi
	if [ "$ret" != '' ]; then
		echo ' *'
		echo ' * check-output-ignore'
		echo " * check-output-returns: $ret"
		rm -f "$file.output.got"
	fi
	if [ $linear != 0 ]; then
		echo ' *'
		echo ' * check-output-ignore'
		echo ' * check-output-contains: xyz\\\\.'
		echo ' * check-output-excludes: \\\\.'
	fi
	for stream in output error; do
		if [ -s "$file.$stream.got" ]; then
			echo " *"
			echo " * check-$stream-start"
			cat "$file.$stream.got"
			echo " * check-$stream-end"
		fi
	done
	echo " */"
	return 0
}

## allow flags from environment
set -- $SPARSE_TEST_FLAGS "$@"

## process the flags
while [ "$#" -gt "0" ]; do
	case "$1" in
	-a|--abort)
		abort=1
		;;
	-q|--quiet)
		vquiet=1
		;;
	--args=*)
		default_args="${1#--args=}";
		;;

	single|--single)
		arg_file "$2"
		do_test "$2"
		case "$?" in
			0) echo "$2 passed !";;
			1) echo "$2 failed !";;
			2) echo "$2 can't be handled by $prog_name";;
		esac
		exit $failed
		;;
	format|--format)
		shift
		do_format "$@"
		exit 0
		;;
	help)
		do_usage
		exit 1
		;;

	*.c|*.cdoc)
		tests_list="$tests_list $1"
		;;
	*)
		if [ ! -d "$1" ]; then
			do_usage
			exit 1
		fi
		tests_list="$tests_list $(find "$1" -name '*.c' | sort)"
		;;
	esac
	shift
done

if [ -z "$tests_list" ]; then
	tests_list=`find . -name '*.c' | sed -e 's#^\./\(.*\)#\1#' | sort`
fi

do_test_suite
exit $failed

